<?php
/**
 * WanlCaptcha 1.0.0
 */
namespace addons\wanlshop\library\WanlSdk;

use think\Exception;
use think\Session;
use think\Request;

class Captcha 
{
    // 验证码加密盐
    private $saltKey = 'WanlShop.COM';
	
    private $codeSet = '3456789ABCDEFGHJKLMNPQRTUVWXY';
    private $fontSize = 26; // 验证码字体大小(px)
	
	private $canvasSize = 200; // 画布大小
	private $randomPoint = 200; // 随机点数量
	private $randomLine = 50; // 随机线条数量
	
    private $_image = null; // 验证码图片实例
    private $_color = null; // 验证码字体颜色
	
    /**
     * 判断当前用户是否已完成验证码效验，且在验证码有效期内
     */
    public function check($endCode = false, $id = '') {
        $config = get_addon_config('wanlshop');
        if ($config['captcha']['captcha_switch'] == 'Y') {
            if (!$endCode) {
                $key = $this->authcode($this->saltKey) . $id . '_log';
                $secode = Session::get($key, '');
                // 判断
                if (!isset($secode) || $secode['captcha_use'] >= $config['captcha']['captchaUseMaxNum'] || $secode['captcha_time'] + $config['captcha']['captchaUseMaxTime'] < time()) {
                    return false;
                } else {
                    Session::set($key . '.captcha_use', $secode['captcha_use'] + 1, '');
                }
                return true;
            } else {
                return false;
            }
        } else {
            return true;
        }
    }
	
	
    /**
     * 输出验证码并把验证码的值保存的session中
     * @access public
     * @param string $id 要生成验证码的标识
     */
    public function image($id = '') {
        $request = Request::instance();
        $config = get_addon_config('wanlshop');
        // 查询判断
        $loglist = model('app\admin\model\wanlshop\CaptchaLog')->where(['ip' => $request->ip() ])->select();
        $hourError = 0;
        $hourAll = 0;
        $dayError = 0;
        $dayAll = 0;
        foreach ($loglist as $vo) {
            // 计算一小时内
            if ($vo['createtime'] > (time() - 60 * 60)) {
                $hourAll+= 1;
                $hourError+= $vo['times'];
            }
            // 计算一天内，当天凌晨计算
            if ($vo['createtime'] > strtotime(date('Y-m-d 00:00:00'))) {
                $dayAll+= 1;
                $dayError+= $vo['times'];
            }
        }
        //单IP一小时内允许出错多少次
        if ($hourError > $config['captcha']['ipHourError']) {
            return $this->result('10001:验证频繁请一小时后再试');
        }
        //单IP一小时内允许生成多少张验证码
        if ($hourAll > $config['captcha']['ipHourAll']) {
            return $this->result('10002:验证频繁请一小时后再试');
        }
        //单IP一天允许验证出错次数
        if ($dayError > $config['captcha']['ipDayError']) {
            return $this->result('10003:验证频繁请明天后再试');
        }
        //单IP一天允许生成多少次验证码
        if ($dayAll > $config['captcha']['ipDayAll']) {
            return $this->result('10004:验证频繁请明天后再试');
        }
        // 获取一张图库图片
        $image = model('app\admin\model\wanlshop\Captcha')->orderRaw('rand()')->limit(1)->find();
        if (!$image) {
            return $this->result('10005:人机验证图库暂无图片');
        }
        // 拼接验证码原图路径
        $capImgFile = $_SERVER['DOCUMENT_ROOT'] . $image['file'];
        //验证码生成后的临时路径
        $outImg = TEMP_PATH . md5(time() . '|' . rand(0, 999) . '|' . rand(0, 999) . $capImgFile) . '.png';
        //验证码随机旋转角度
        $angle = (int)rand(0, 360);
		//验证码原图
		$this->canvasSize = $config['captcha']['canvasSize']; // 画布大小
		$this->randomPoint = $config['captcha']['randomPoint']; // 随机点数量
		$this->randomLine = $config['captcha']['randomLine']; // 随机线条数量
        // 服务端生成验证图片 
		// [验证码原图] [验证码输出地址] [验证码图旋转角度] [画布大小] [随机点数量] [随机线条数量] [随机大矩形数量]
        if ($config['captcha']['captchaService'] == 'node') {
			$node = ROOT_PATH .'node' .DS. 'captcha' .DS. 'index.js';
            $cmd = "{$config['captcha']['nodePath']} {$node} {$capImgFile} {$outImg} {$angle} {$this->canvasSize} {$this->randomPoint} {$this->randomLine} {$config['captcha']['randomBlock']}";
			if (intval(shell_exec($cmd)) !== 200) {
                return $this->result('请按文档配置正确node效验', false, 5);
            }
        } else {
            $captchaGd = $this->_captchaGd($capImgFile, $outImg, $angle);
            if (!$captchaGd) {
                return $this->result('服务器生成验证图像失败', false, 5);
            }
        }
        // 验证码图片转base64
        $imgBase64 = base64_encode(file_get_contents($outImg));
        //删除临时验证码
        unlink($outImg);
        // 记录日志
        $log = model('app\admin\model\wanlshop\CaptchaLog');
        $log->captcha_id = $image['id'];
        $log->angle = $angle;
        $log->ip = $request->ip();
        $log->save();
        if (!$log) {
            return $this->result('10007:网络繁忙请稍后重试');
        }
        if (!$image->setInc('times')) {
            return $this->result('10008:网络繁忙请稍后重试');
        }
        // 保存验证码
        $key = $this->authcode($this->saltKey) . $id;
        $angle = $this->authcode($angle);
        $secode = ['verify_id' => $log['id'], 'verify_code' => $angle, 'verify_time' => time() ];
        Session::set($key, $secode, '');
        return $this->result('获取图片成功', $imgBase64, 0);
    }
	
	
    /**
     * 验证验证图
     * @param string $rotationAngle 旋转角度
     * @param string $mouseTrackList 滑动轨迹
     * @param string $dragUseTime 拖动用时
     * @param string $dragStartTime 拖动开始时间
     */
    public function checkCaptcha($rotationAngle, $mouseTrackList, $dragUseTime, $dragStartTime, $id = '') 
	{
        $config = get_addon_config('wanlshop');
        $key = $this->authcode($this->saltKey) . $id;
        // 验证码不能为空
        $secode = Session::get($key, '');
        if (empty($secode)) {
            return $this->result('系统无法获取到Session');
        }
        if (empty($rotationAngle) || empty($secode)) {
            return $this->result('未安装node或者未按文档初始', true);
        }
        //缺少角度值，不进行数据效验
        if (!isset($rotationAngle)) {
            return $this->result('11002:人机验证失败', true);
        } else {
            $rotationAngle = (int)$rotationAngle;
        }
        if ($rotationAngle < 0 || $rotationAngle > 360) {
            return $this->result('12001:请拖动旋转图像');
        } else {
            //旋转角度,四舍五入
            $rotationAngle = (int)round(360 - $rotationAngle);
        }
        if (!isset($dragUseTime)) {
            return $this->result('12002:人机效验数据异常');
        } else {
            $dragUseTime = (int)$dragUseTime;
        }
        if (!isset($dragStartTime)) {
            return $this->result('12003:人机效验数据异常');
        } else {
            $dragStartTime = (int)$dragStartTime / 1000;
        }
        if (!isset($mouseTrackList) || !$mouseTrackList) {
            return $this->result('12004:人机效验数据异常');
        } else {
            $mouseTrackList = $this->getJson((string)$mouseTrackList);
        }
        // 查询记录
        $log = model('app\admin\model\wanlshop\CaptchaLog')->get($secode['verify_id']);
        if (!$log) {
            return $this->result('11003:网络繁忙请稍后再试', true);
        }
        //验证次数超出限制
        if ($log['times'] >= $config['captcha']['oneCapErrNum']) {
            return $this->result('验证失误过多系统将更换图片请重试', true);
        }
        //验证码超时时间效验
        if ($log['createtime'] + $config['captcha']['checkTimeOut'] < time() || $secode['verify_time'] + $config['captcha']['checkTimeOut'] < time()) {
            Session::delete($key, '');
            return $this->result('11005:效验超时请重试', true);
        }
        $captchaCheckOutTime = $log['createtime'] + $config['captcha']['checkTimeOut'];
        // 拖拽用时
        if ($dragUseTime > $config['captcha']['dragTimeMax'] || $dragUseTime < $config['captcha']['dragTimeMin']) {
            return $this->result('拖拽用时"过快"或"过慢" ~', true);
        }
        $dragUseTime = $dragUseTime / 1000;
        // $dragStartTime < $log['createtime'] ||
        if (($dragStartTime + $dragUseTime) > $captchaCheckOutTime) {
            return $this->result('人机效验超时请重试', true);
        }
        /**
         * 鼠标轨迹解析
         */
        if (!$mouseTrackList || count($mouseTrackList) < 2) {
            return $this->result('11008:拖拽过快请重试', true);
        }
        foreach ($mouseTrackList as $index => $item) {
            if (!isset($item['r']) || !isset($item['t'])) {
                return $this->result('11009:人机验证失败', true);
            }
            $item['t'] = $item['t'] / 1000;
            //转为秒单位
            if ($item['t'] < $dragStartTime) {
                return $this->result('11010:人机验证失败', true);
            }
            if ($item['t'] > ($dragUseTime + $dragStartTime)) {
                return $this->result('11011:人机验证失败', true);
            }
            $lastTime = $index == 0 ? $dragStartTime : $mouseTrackList[$index - 1]['t'] / 1000;
            if ($item['t'] < $lastTime + ($config['captcha']['dragInterval'] / 1000)) {
                return $this->result('11012:人机验证失败', true);
            }
            $item['r'] = (int)round($item['r']);
            if ($item['r'] < 0 || $item['r'] > 100) {
                return $this->result('11013:人机验证失败', true);
            }
        }
        if (!in_array($rotationAngle, $this->getSuccessRotationAngle($log['angle']))) {
            //角度效验失败
            if (!$log->setInc('times')) {
                return $this->result('12005:网络繁忙请稍后重试');
            }
            if ($log['times'] >= $config['captcha']['oneCapErrNum']) {
                return $this->result('验证失误过多系统将更换图片请重试', true);
            }
            return $this->result('验证失败，请重试 ~');
        } else {
            //角度效验成功
            if (!$log->save(['updatetime' => time() , 'succeedtime' => $dragUseTime])) {
                return $this->result('12007:网络繁忙请稍后重试');
            }
            // 删除session
            Session::delete($key, '');
            //记录验证码使用次数
            Session::set($key . '_log', ['captcha_use' => 0, 'captcha_time' => time() ], '');
            return $this->result('人机验证效验成功', false, 0);
        }
    }
	
	
    /**
     * 获取可通过的角度列表
     * @param {Object} $rotationAngle 角度列表
     */
    private function getSuccessRotationAngle($rotationAngle) {
        $config = get_addon_config('wanlshop');
        $yesArray = [$rotationAngle];
        for ($i = 0; $i < $config['captcha']['errorAccuracy']; $i++) {
            $yesArray[] = $rotationAngle - ($i + 1);
            $yesArray[] = $rotationAngle + ($i + 1);
        }
        foreach ($yesArray as $index => $value) {
            if ($value < 0) {
                $yesArray[$index] = 360 + $value;
            } else if ($value > 360) {
                $yesArray[$index] = $value - 360;
            }
        }
        if (in_array(0, $yesArray) && !in_array(360, $yesArray)) {
            $yesArray[] = 360;
        }
        if (!in_array(0, $yesArray) && in_array(360, $yesArray)) {
            $yesArray[] = 0;
        }
        return $yesArray;
    }
	
	/**
	 * php 生成验证图像
	 *
	 * @param {Object} $file 验证码原图
	 * @param {Object} $outImg 验证码输出地址
	 */
	private function _captchaGd($file = '', $outImg = '', $angle = 0) {
	    // 判断图片格式
	    $imageSize = getimagesize($file);
	    $imageSize = explode('/', $imageSize['mime']);
	    $type = $imageSize[1];
	    // 由文件创建图片
	    switch ($type) {
	        case "png":
	            $this->_image = imagecreatefrompng($file);
	            break;
	        case "jpeg":
	            $this->_image = imagecreatefromjpeg($file);
	            break;
	        case "jpg":
	            $this->_image = imagecreatefromjpeg($file);
	            break;
	        case "gif":
	            $this->_image = imagecreatefromgif($file);
	            break;
	    }
	    // 切图、旋转、再切图
	    return self::_cutSquares($outImg, $angle);
	}
	
	// 首次裁切
	private  function _cutSquares($outImg = '', $angle = 0) {
	    $w = imagesx($this->_image);
	    $h = imagesy($this->_image);
	    if ($w > $h) {
	        $new_height = $this->canvasSize;
	        $new_width = floor($w * ($new_height / $h));
	        $crop_x = ceil(($w - $h) / 2);
	        $crop_y = 0;
	    } else {
	        $new_width = $this->canvasSize;
	        $new_height = floor($h * ($new_width / $w));
	        $crop_x = 0;
	        $crop_y = ceil(($h - $w) / 2);
	    }
	    $tmp_img = imagecreatetruecolor($this->canvasSize, $this->canvasSize);
	    imagecopyresampled($tmp_img, $this->_image, 0, 0, $crop_x, $crop_y, $new_width, $new_height, $w, $h);
	    // 旋转角度
	    $this->_image = imagerotate($tmp_img, -$angle, 0);
	    // 销毁一图像
	    imagedestroy($tmp_img);
		return self::_cuttingSquaretwice($outImg);
	}
	
	
	// 二次裁切
	private  function _cuttingSquaretwice($outImg = '') {
	    $w = imagesx($this->_image);
	    $h = imagesy($this->_image);
	    $new_width = $w * 2 * $this->canvasSize / $w;
	    $new_height = $h * 2 * $this->canvasSize / $w;
	    $crop_x = $w / 4;
	    $crop_y = $h / 4;
	    $tmp_img = imagecreatetruecolor($this->canvasSize, $this->canvasSize);
	    imagecopyresampled($tmp_img, $this->_image , 0, 0, $crop_x, $crop_y, $new_width, $new_height, $w, $h);
	    $this->_image = $tmp_img;
	    // 销毁一图像
	    // imagedestroy($tmp_img);
		// 画曲线
		return self::_writeCurve($outImg);
	}
	
	/**
	 * 画一条由两条连在一起构成的随机正弦函数曲线作干扰线(你可以改成更帅的曲线函数)
	 *      正弦型函数解析式：y=Asin(ωx+φ)+b
	 *      各常数值对函数图像的影响：
	 *        A：决定峰值（即纵向拉伸压缩的倍数）
	 *        b：表示波形在Y轴的位置关系或纵向移动距离（上加下减）
	 *        φ：决定波形与X轴位置关系或横向移动距离（左加右减）
	 *        ω：决定周期（最小正周期T=2π/∣ω∣）
	 */
	private  function _writeCurve($outImg = '') {
		$color = imagecolorallocate($this->_image, mt_rand(1, 120) , mt_rand(1, 120) , mt_rand(1, 120));
	    $A = mt_rand(1, $this->canvasSize / 2); // 振幅
	    $b = mt_rand(-$this->canvasSize / 4, $this->canvasSize / 4); // Y轴方向偏移量
	    $f = mt_rand(-$this->canvasSize / 4, $this->canvasSize / 4); // X轴方向偏移量
	    $T = mt_rand($this->canvasSize * 1.5, $this->canvasSize * 2); // 周期
	    $w = (2 * M_PI) / $T;
	    $px1 = 0; // 曲线横坐标起始位置
	    $px2 = mt_rand($this->canvasSize / 2, $this->canvasSize * 0.667); // 曲线横坐标结束位置
	    for ($px = $px1; $px <= $px2; $px = $px + 0.9) {
	        if ($w != 0) {
	            $py = $A * sin($w * $px + $f) + $b + $this->canvasSize / 2; // y = Asin(ωx+φ) + b
	            $i = (int)(($this->fontSize - 6) / 4);
	            while ($i > 0) {
	                imagesetpixel($this->_image, $px + $i, $py + $i, $color);
	                //这里画像素点比imagettftext和imagestring性能要好很多
	                $i--;
	            }
	        }
	    }
	    $A = mt_rand(1, $this->canvasSize / 2); // 振幅
	    $f = mt_rand(-$this->canvasSize / 4, $this->canvasSize / 4); // X轴方向偏移量
	    $T = mt_rand($this->canvasSize * 1.5, $this->canvasSize * 2); // 周期
	    $w = (2 * M_PI) / $T;
	    $b = $py - $A * sin($w * $px + $f) - $this->canvasSize / 2;
	    $px1 = $px2;
	    $px2 = $this->canvasSize;
	    for ($px = $px1; $px <= $px2; $px = $px + 0.9) {
	        if ($w != 0) {
	            $py = $A * sin($w * $px + $f) + $b + $this->canvasSize / 2; // y = Asin(ωx+φ) + b
	            $i = (int)(($this->fontSize - 8) / 4);
	            while ($i > 0) {
	                imagesetpixel($this->_image, $px + $i, $py + $i, $color);
	                $i--;
	            }
	        }
	    }
	    // 画背景
	    return self::_writeNoise($outImg);
	}
	
	
	/**  
	 * 画杂点
	 * 往图片上写不同颜色的字母或数字
	 */
	private  function _writeNoise($outImg = '') {
	    for ($i = 0; $i < 10; $i++) {
	        //杂点颜色
	        $noiseColor = imagecolorallocate($this->_image, mt_rand(150, 225) , mt_rand(150, 225) , mt_rand(150, 225));
	        for ($j = 0; $j < 5; $j++) {
	            // 绘杂点
	            imagestring($this->_image, 5, mt_rand(-10, $this->canvasSize) , mt_rand(-10, $this->canvasSize) , $this->codeSet[mt_rand(0, 28) ], // 杂点文本为随机的字母或数字
	            $noiseColor);
	        }
	    }
	    return self::_writeLine($outImg);
	}
	
	// 画线
	private  function _writeLine($outImg = '') {
	    //画干扰点
	    for ($i = 0; $i < $this->randomPoint; $i++) {
	        //设置随机颜色
	        $randColor = imagecolorallocate($this->_image, rand(0, 255) , rand(0, 255) , rand(0, 255));
	        //画点
	        imagesetpixel($this->_image, rand(1, $this->canvasSize) , rand(1, $this->canvasSize) , $randColor);
	    }
	    //画干扰线
	    for ($i = 0; $i < $this->randomLine; $i++) {
	        //设置随机颜色
	        $randColor = imagecolorallocate($this->_image, rand(0, 200) , rand(0, 200) , rand(0, 200));
	        //画线
	        imageline($this->_image, rand(1, $this->canvasSize) , rand(1, $this->canvasSize) , rand(1, $this->canvasSize) , rand(1, $this->canvasSize) , $randColor);
	    }
		return self::_cutRound($outImg);
	}
	
	
	// 裁切圆形
	private  function _cutRound($outImg = '') {
	    //创建新图
	    $tmp_img = imagecreatetruecolor($this->canvasSize, $this->canvasSize);
	    // 启用混色模式
	    imagealphablending($tmp_img, false); //设定图像的混色模式
	    $transparent = imagecolorallocatealpha($tmp_img, 255, 255, 255, 127); //边缘透明
	    $w = $this->canvasSize;
	    $h = $this->canvasSize;
	    $w = min($w, $h);
	    $h = $w;
	    $r = $w / 2;
	    for ($x = 0; $x < $w; $x++) for ($y = 0; $y < $h; $y++) {
	        $c = imagecolorat($this->_image, $x, $y);
	        $_x = $x - $w / 2;
	        $_y = $y - $h / 2;
	        if ((($_x * $_x) + ($_y * $_y)) < ($r * $r)) {
	            imagesetpixel($tmp_img, $x, $y, $c);
	        } else {
	            imagesetpixel($tmp_img, $x, $y, $transparent);
	        }
	    }
	    $png = imagepng($tmp_img, $outImg);
	    // 销毁一图像
	    imagedestroy($this->_image);
	    imagedestroy($tmp_img);
		return $png;
	}
	
	
    /**
     * 加密验证码
     * @param {Object} $str 字符串
     */
    private function authcode($str) {
        $config = get_addon_config('wanlshop');
        $key = substr(md5($config['captcha']['seKey']) , 5, 8);
        $str = substr(md5($str) , 8, 10);
        return md5($key . $str);
    }
	
	
    /**
     * json_decode 二次封装
     * @param {Object} $json
     */
    private function getJson($json) {
        if (is_array($json)) {
            return $json;
        }
        if (!$json) {
            return false;
        }
        $rt = false;
        try {
            $rt = json_decode($json, true);
        }
        catch(Exception $th) {
            var_dump($th);
        }
        if (!is_array($rt)) {
            return false;
        }
        return $rt;
    }
	
	
    /**
     * 结束代码并返回需要刷新验证码
     */
    public function result($msg = '请先完成人机效验!', $data = false, $code = 4) {
        return ['code' => $code, 'data' => $data, 'msg' => $msg];
    }
}